<h1 id="自定义控件">自定义控件</h1>
<p>PyQt5有丰富的组件，但是肯定满足不了所有开发者的所有需求，PyQt5只提供了基本的组件，像按钮，文本，滑块等。如果你还需要其他的模块，应该尝试自己去自定义一些。</p>
<p>自定义组件使用绘画工具创建，有两个基本方式：根据已有的创建或改进；通过自己绘图创建。</p>
<h2 id="burning-widget">Burning widget</h2>
<p>这个组件我们会在Nero，K3B，或者其他CD/DVD烧录软件中见到。</p>
<div class="sourceCode" id="cb1"><pre><code class="language-python"><a class="sourceLine" id="cb1-1" data-line-number="1"><span class="co">#!/usr/bin/python3</span></a>
<a class="sourceLine" id="cb1-2" data-line-number="2"><span class="co"># -*- coding: utf-8 -*-</span></a>
<a class="sourceLine" id="cb1-3" data-line-number="3"></a>
<a class="sourceLine" id="cb1-4" data-line-number="4"><span class="co">&quot;&quot;&quot;</span></a>
<a class="sourceLine" id="cb1-5" data-line-number="5"><span class="co">ZetCode PyQt5 tutorial </span></a>
<a class="sourceLine" id="cb1-6" data-line-number="6"></a>
<a class="sourceLine" id="cb1-7" data-line-number="7"><span class="co">In this example, we create a custom widget.</span></a>
<a class="sourceLine" id="cb1-8" data-line-number="8"></a>
<a class="sourceLine" id="cb1-9" data-line-number="9"><span class="co">Author: Jan Bodnar</span></a>
<a class="sourceLine" id="cb1-10" data-line-number="10"><span class="co">Website: zetcode.com </span></a>
<a class="sourceLine" id="cb1-11" data-line-number="11"><span class="co">Last edited: August 2017</span></a>
<a class="sourceLine" id="cb1-12" data-line-number="12"><span class="co">&quot;&quot;&quot;</span></a>
<a class="sourceLine" id="cb1-13" data-line-number="13"></a>
<a class="sourceLine" id="cb1-14" data-line-number="14"><span class="im">from</span> PyQt5.QtWidgets <span class="im">import</span> (QWidget, QSlider, QApplication, </a>
<a class="sourceLine" id="cb1-15" data-line-number="15">    QHBoxLayout, QVBoxLayout)</a>
<a class="sourceLine" id="cb1-16" data-line-number="16"><span class="im">from</span> PyQt5.QtCore <span class="im">import</span> QObject, Qt, pyqtSignal</a>
<a class="sourceLine" id="cb1-17" data-line-number="17"><span class="im">from</span> PyQt5.QtGui <span class="im">import</span> QPainter, QFont, QColor, QPen</a>
<a class="sourceLine" id="cb1-18" data-line-number="18"><span class="im">import</span> sys</a>
<a class="sourceLine" id="cb1-19" data-line-number="19"></a>
<a class="sourceLine" id="cb1-20" data-line-number="20"><span class="kw">class</span> Communicate(QObject):</a>
<a class="sourceLine" id="cb1-21" data-line-number="21">    </a>
<a class="sourceLine" id="cb1-22" data-line-number="22">    updateBW <span class="op">=</span> pyqtSignal(<span class="bu">int</span>)</a>
<a class="sourceLine" id="cb1-23" data-line-number="23"></a>
<a class="sourceLine" id="cb1-24" data-line-number="24"></a>
<a class="sourceLine" id="cb1-25" data-line-number="25"><span class="kw">class</span> BurningWidget(QWidget):</a>
<a class="sourceLine" id="cb1-26" data-line-number="26">  </a>
<a class="sourceLine" id="cb1-27" data-line-number="27">    <span class="kw">def</span> <span class="fu">__init__</span>(<span class="va">self</span>):      </a>
<a class="sourceLine" id="cb1-28" data-line-number="28">        <span class="bu">super</span>().<span class="fu">__init__</span>()</a>
<a class="sourceLine" id="cb1-29" data-line-number="29">        </a>
<a class="sourceLine" id="cb1-30" data-line-number="30">        <span class="va">self</span>.initUI()</a>
<a class="sourceLine" id="cb1-31" data-line-number="31">        </a>
<a class="sourceLine" id="cb1-32" data-line-number="32">        </a>
<a class="sourceLine" id="cb1-33" data-line-number="33">    <span class="kw">def</span> initUI(<span class="va">self</span>):</a>
<a class="sourceLine" id="cb1-34" data-line-number="34">        </a>
<a class="sourceLine" id="cb1-35" data-line-number="35">        <span class="va">self</span>.setMinimumSize(<span class="dv">1</span>, <span class="dv">30</span>)</a>
<a class="sourceLine" id="cb1-36" data-line-number="36">        <span class="va">self</span>.value <span class="op">=</span> <span class="dv">75</span></a>
<a class="sourceLine" id="cb1-37" data-line-number="37">        <span class="va">self</span>.num <span class="op">=</span> [<span class="dv">75</span>, <span class="dv">150</span>, <span class="dv">225</span>, <span class="dv">300</span>, <span class="dv">375</span>, <span class="dv">450</span>, <span class="dv">525</span>, <span class="dv">600</span>, <span class="dv">675</span>]</a>
<a class="sourceLine" id="cb1-38" data-line-number="38"></a>
<a class="sourceLine" id="cb1-39" data-line-number="39"></a>
<a class="sourceLine" id="cb1-40" data-line-number="40">    <span class="kw">def</span> setValue(<span class="va">self</span>, value):</a>
<a class="sourceLine" id="cb1-41" data-line-number="41"></a>
<a class="sourceLine" id="cb1-42" data-line-number="42">        <span class="va">self</span>.value <span class="op">=</span> value</a>
<a class="sourceLine" id="cb1-43" data-line-number="43"></a>
<a class="sourceLine" id="cb1-44" data-line-number="44"></a>
<a class="sourceLine" id="cb1-45" data-line-number="45">    <span class="kw">def</span> paintEvent(<span class="va">self</span>, e):</a>
<a class="sourceLine" id="cb1-46" data-line-number="46">      </a>
<a class="sourceLine" id="cb1-47" data-line-number="47">        qp <span class="op">=</span> QPainter()</a>
<a class="sourceLine" id="cb1-48" data-line-number="48">        qp.begin(<span class="va">self</span>)</a>
<a class="sourceLine" id="cb1-49" data-line-number="49">        <span class="va">self</span>.drawWidget(qp)</a>
<a class="sourceLine" id="cb1-50" data-line-number="50">        qp.end()</a>
<a class="sourceLine" id="cb1-51" data-line-number="51">      </a>
<a class="sourceLine" id="cb1-52" data-line-number="52">      </a>
<a class="sourceLine" id="cb1-53" data-line-number="53">    <span class="kw">def</span> drawWidget(<span class="va">self</span>, qp):</a>
<a class="sourceLine" id="cb1-54" data-line-number="54">        </a>
<a class="sourceLine" id="cb1-55" data-line-number="55">        MAX_CAPACITY <span class="op">=</span> <span class="dv">700</span></a>
<a class="sourceLine" id="cb1-56" data-line-number="56">        OVER_CAPACITY <span class="op">=</span> <span class="dv">750</span></a>
<a class="sourceLine" id="cb1-57" data-line-number="57">      </a>
<a class="sourceLine" id="cb1-58" data-line-number="58">        font <span class="op">=</span> QFont(<span class="st">&#39;Serif&#39;</span>, <span class="dv">7</span>, QFont.Light)</a>
<a class="sourceLine" id="cb1-59" data-line-number="59">        qp.setFont(font)</a>
<a class="sourceLine" id="cb1-60" data-line-number="60"></a>
<a class="sourceLine" id="cb1-61" data-line-number="61">        size <span class="op">=</span> <span class="va">self</span>.size()</a>
<a class="sourceLine" id="cb1-62" data-line-number="62">        w <span class="op">=</span> size.width()</a>
<a class="sourceLine" id="cb1-63" data-line-number="63">        h <span class="op">=</span> size.height()</a>
<a class="sourceLine" id="cb1-64" data-line-number="64"></a>
<a class="sourceLine" id="cb1-65" data-line-number="65">        step <span class="op">=</span> <span class="bu">int</span>(<span class="bu">round</span>(w <span class="op">/</span> <span class="dv">10</span>))</a>
<a class="sourceLine" id="cb1-66" data-line-number="66"></a>
<a class="sourceLine" id="cb1-67" data-line-number="67"></a>
<a class="sourceLine" id="cb1-68" data-line-number="68">        till <span class="op">=</span> <span class="bu">int</span>(((w <span class="op">/</span> OVER_CAPACITY) <span class="op">*</span> <span class="va">self</span>.value))</a>
<a class="sourceLine" id="cb1-69" data-line-number="69">        full <span class="op">=</span> <span class="bu">int</span>(((w <span class="op">/</span> OVER_CAPACITY) <span class="op">*</span> MAX_CAPACITY))</a>
<a class="sourceLine" id="cb1-70" data-line-number="70"></a>
<a class="sourceLine" id="cb1-71" data-line-number="71">        <span class="cf">if</span> <span class="va">self</span>.value <span class="op">&gt;=</span> MAX_CAPACITY:</a>
<a class="sourceLine" id="cb1-72" data-line-number="72">            </a>
<a class="sourceLine" id="cb1-73" data-line-number="73">            qp.setPen(QColor(<span class="dv">255</span>, <span class="dv">255</span>, <span class="dv">255</span>))</a>
<a class="sourceLine" id="cb1-74" data-line-number="74">            qp.setBrush(QColor(<span class="dv">255</span>, <span class="dv">255</span>, <span class="dv">184</span>))</a>
<a class="sourceLine" id="cb1-75" data-line-number="75">            qp.drawRect(<span class="dv">0</span>, <span class="dv">0</span>, full, h)</a>
<a class="sourceLine" id="cb1-76" data-line-number="76">            qp.setPen(QColor(<span class="dv">255</span>, <span class="dv">175</span>, <span class="dv">175</span>))</a>
<a class="sourceLine" id="cb1-77" data-line-number="77">            qp.setBrush(QColor(<span class="dv">255</span>, <span class="dv">175</span>, <span class="dv">175</span>))</a>
<a class="sourceLine" id="cb1-78" data-line-number="78">            qp.drawRect(full, <span class="dv">0</span>, till<span class="op">-</span>full, h)</a>
<a class="sourceLine" id="cb1-79" data-line-number="79">            </a>
<a class="sourceLine" id="cb1-80" data-line-number="80">        <span class="cf">else</span>:</a>
<a class="sourceLine" id="cb1-81" data-line-number="81">            </a>
<a class="sourceLine" id="cb1-82" data-line-number="82">            qp.setPen(QColor(<span class="dv">255</span>, <span class="dv">255</span>, <span class="dv">255</span>))</a>
<a class="sourceLine" id="cb1-83" data-line-number="83">            qp.setBrush(QColor(<span class="dv">255</span>, <span class="dv">255</span>, <span class="dv">184</span>))</a>
<a class="sourceLine" id="cb1-84" data-line-number="84">            qp.drawRect(<span class="dv">0</span>, <span class="dv">0</span>, till, h)</a>
<a class="sourceLine" id="cb1-85" data-line-number="85"></a>
<a class="sourceLine" id="cb1-86" data-line-number="86"></a>
<a class="sourceLine" id="cb1-87" data-line-number="87">        pen <span class="op">=</span> QPen(QColor(<span class="dv">20</span>, <span class="dv">20</span>, <span class="dv">20</span>), <span class="dv">1</span>, </a>
<a class="sourceLine" id="cb1-88" data-line-number="88">            Qt.SolidLine)</a>
<a class="sourceLine" id="cb1-89" data-line-number="89">            </a>
<a class="sourceLine" id="cb1-90" data-line-number="90">        qp.setPen(pen)</a>
<a class="sourceLine" id="cb1-91" data-line-number="91">        qp.setBrush(Qt.NoBrush)</a>
<a class="sourceLine" id="cb1-92" data-line-number="92">        qp.drawRect(<span class="dv">0</span>, <span class="dv">0</span>, w<span class="dv">-1</span>, h<span class="dv">-1</span>)</a>
<a class="sourceLine" id="cb1-93" data-line-number="93"></a>
<a class="sourceLine" id="cb1-94" data-line-number="94">        j <span class="op">=</span> <span class="dv">0</span></a>
<a class="sourceLine" id="cb1-95" data-line-number="95"></a>
<a class="sourceLine" id="cb1-96" data-line-number="96">        <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(step, <span class="dv">10</span><span class="op">*</span>step, step):</a>
<a class="sourceLine" id="cb1-97" data-line-number="97">          </a>
<a class="sourceLine" id="cb1-98" data-line-number="98">            qp.drawLine(i, <span class="dv">0</span>, i, <span class="dv">5</span>)</a>
<a class="sourceLine" id="cb1-99" data-line-number="99">            metrics <span class="op">=</span> qp.fontMetrics()</a>
<a class="sourceLine" id="cb1-100" data-line-number="100">            fw <span class="op">=</span> metrics.width(<span class="bu">str</span>(<span class="va">self</span>.num[j]))</a>
<a class="sourceLine" id="cb1-101" data-line-number="101">            qp.drawText(i<span class="op">-</span>fw<span class="op">/</span><span class="dv">2</span>, h<span class="op">/</span><span class="dv">2</span>, <span class="bu">str</span>(<span class="va">self</span>.num[j]))</a>
<a class="sourceLine" id="cb1-102" data-line-number="102">            j <span class="op">=</span> j <span class="op">+</span> <span class="dv">1</span></a>
<a class="sourceLine" id="cb1-103" data-line-number="103">            </a>
<a class="sourceLine" id="cb1-104" data-line-number="104"></a>
<a class="sourceLine" id="cb1-105" data-line-number="105"><span class="kw">class</span> Example(QWidget):</a>
<a class="sourceLine" id="cb1-106" data-line-number="106">    </a>
<a class="sourceLine" id="cb1-107" data-line-number="107">    <span class="kw">def</span> <span class="fu">__init__</span>(<span class="va">self</span>):</a>
<a class="sourceLine" id="cb1-108" data-line-number="108">        <span class="bu">super</span>().<span class="fu">__init__</span>()</a>
<a class="sourceLine" id="cb1-109" data-line-number="109">        </a>
<a class="sourceLine" id="cb1-110" data-line-number="110">        <span class="va">self</span>.initUI()</a>
<a class="sourceLine" id="cb1-111" data-line-number="111">        </a>
<a class="sourceLine" id="cb1-112" data-line-number="112">        </a>
<a class="sourceLine" id="cb1-113" data-line-number="113">    <span class="kw">def</span> initUI(<span class="va">self</span>):      </a>
<a class="sourceLine" id="cb1-114" data-line-number="114">        </a>
<a class="sourceLine" id="cb1-115" data-line-number="115">        OVER_CAPACITY <span class="op">=</span> <span class="dv">750</span></a>
<a class="sourceLine" id="cb1-116" data-line-number="116"></a>
<a class="sourceLine" id="cb1-117" data-line-number="117">        sld <span class="op">=</span> QSlider(Qt.Horizontal, <span class="va">self</span>)</a>
<a class="sourceLine" id="cb1-118" data-line-number="118">        sld.setFocusPolicy(Qt.NoFocus)</a>
<a class="sourceLine" id="cb1-119" data-line-number="119">        sld.setRange(<span class="dv">1</span>, OVER_CAPACITY)</a>
<a class="sourceLine" id="cb1-120" data-line-number="120">        sld.setValue(<span class="dv">75</span>)</a>
<a class="sourceLine" id="cb1-121" data-line-number="121">        sld.setGeometry(<span class="dv">30</span>, <span class="dv">40</span>, <span class="dv">150</span>, <span class="dv">30</span>)</a>
<a class="sourceLine" id="cb1-122" data-line-number="122"></a>
<a class="sourceLine" id="cb1-123" data-line-number="123">        <span class="va">self</span>.c <span class="op">=</span> Communicate()        </a>
<a class="sourceLine" id="cb1-124" data-line-number="124">        <span class="va">self</span>.wid <span class="op">=</span> BurningWidget()</a>
<a class="sourceLine" id="cb1-125" data-line-number="125">        <span class="va">self</span>.c.updateBW[<span class="bu">int</span>].<span class="ex">connect</span>(<span class="va">self</span>.wid.setValue)</a>
<a class="sourceLine" id="cb1-126" data-line-number="126"></a>
<a class="sourceLine" id="cb1-127" data-line-number="127">        sld.valueChanged[<span class="bu">int</span>].<span class="ex">connect</span>(<span class="va">self</span>.changeValue)</a>
<a class="sourceLine" id="cb1-128" data-line-number="128">        hbox <span class="op">=</span> QHBoxLayout()</a>
<a class="sourceLine" id="cb1-129" data-line-number="129">        hbox.addWidget(<span class="va">self</span>.wid)</a>
<a class="sourceLine" id="cb1-130" data-line-number="130">        vbox <span class="op">=</span> QVBoxLayout()</a>
<a class="sourceLine" id="cb1-131" data-line-number="131">        vbox.addStretch(<span class="dv">1</span>)</a>
<a class="sourceLine" id="cb1-132" data-line-number="132">        vbox.addLayout(hbox)</a>
<a class="sourceLine" id="cb1-133" data-line-number="133">        <span class="va">self</span>.setLayout(vbox)</a>
<a class="sourceLine" id="cb1-134" data-line-number="134">        </a>
<a class="sourceLine" id="cb1-135" data-line-number="135">        <span class="va">self</span>.setGeometry(<span class="dv">300</span>, <span class="dv">300</span>, <span class="dv">390</span>, <span class="dv">210</span>)</a>
<a class="sourceLine" id="cb1-136" data-line-number="136">        <span class="va">self</span>.setWindowTitle(<span class="st">&#39;Burning widget&#39;</span>)</a>
<a class="sourceLine" id="cb1-137" data-line-number="137">        <span class="va">self</span>.show()</a>
<a class="sourceLine" id="cb1-138" data-line-number="138">        </a>
<a class="sourceLine" id="cb1-139" data-line-number="139">        </a>
<a class="sourceLine" id="cb1-140" data-line-number="140">    <span class="kw">def</span> changeValue(<span class="va">self</span>, value):</a>
<a class="sourceLine" id="cb1-141" data-line-number="141">             </a>
<a class="sourceLine" id="cb1-142" data-line-number="142">        <span class="va">self</span>.c.updateBW.emit(value)        </a>
<a class="sourceLine" id="cb1-143" data-line-number="143">        <span class="va">self</span>.wid.repaint()</a>
<a class="sourceLine" id="cb1-144" data-line-number="144">        </a>
<a class="sourceLine" id="cb1-145" data-line-number="145">        </a>
<a class="sourceLine" id="cb1-146" data-line-number="146"><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&#39;__main__&#39;</span>:</a>
<a class="sourceLine" id="cb1-147" data-line-number="147">    </a>
<a class="sourceLine" id="cb1-148" data-line-number="148">    app <span class="op">=</span> QApplication(sys.argv)</a>
<a class="sourceLine" id="cb1-149" data-line-number="149">    ex <span class="op">=</span> Example()</a>
<a class="sourceLine" id="cb1-150" data-line-number="150">    sys.exit(app.exec_())</a></code></pre></div>
<p>本例中，我们使用了<code>QSlider</code>和一个自定义组件，由进度条控制。显示的有物体（也就是CD/DVD）的总容量和剩余容量。进度条的范围是1~750。如果值达到了700（OVER_CAPACITY），就显示为红色，代表了烧毁了的意思。</p>
<p>烧录组件在窗口的底部，这个组件是用<code>QHBoxLayout</code>和<code>QVBoxLayout</code>组成的。</p>
<div class="sourceCode" id="cb2"><pre><code class="language-python"><a class="sourceLine" id="cb2-1" data-line-number="1"><span class="kw">class</span> BurningWidget(QWidget):</a>
<a class="sourceLine" id="cb2-2" data-line-number="2">  </a>
<a class="sourceLine" id="cb2-3" data-line-number="3">    <span class="kw">def</span> <span class="fu">__init__</span>(<span class="va">self</span>):      </a>
<a class="sourceLine" id="cb2-4" data-line-number="4">        <span class="bu">super</span>().<span class="fu">__init__</span>()        </a></code></pre></div>
<p>基于<code>QWidget</code>组件。</p>
<pre><code class="language-python">self.setMinimumSize(1, 30)</code></pre>
<p>修改组件进度条的高度，默认的有点小。</p>
<pre><code class="language-python">font = QFont(&#39;Serif&#39;, 7, QFont.Light)
qp.setFont(font)</code></pre>
<p>使用比默认更小一点的字体，这样更配。</p>
<pre><code class="language-python">size = self.size()
w = size.width()
h = size.height()

step = int(round(w / 10.0))


till = int(((w / 750.0) * self.value))
full = int(((w / 750.0) * 700))</code></pre>
<p>动态的渲染组件，随着窗口的大小而变化，这就是我们计算窗口大小的原因。最后一个参数决定了组件的最大范围，进度条的值是由窗口大小按比例计算出来的。最大值的地方填充的是红色。注意这里使用的是浮点数，能提高计算和渲染的精度。</p>
<p>绘画由三部分组成，黄色或红色区域和黄色矩形，然后是分割线，最后是添上代表容量的数字。</p>
<pre><code class="language-python">metrics = qp.fontMetrics()
fw = metrics.width(str(self.num[j]))
qp.drawText(i-fw/2, h/2, str(self.num[j]))</code></pre>
<p>这里使用字体去渲染文本。必须要知道文本的宽度，这样才能让文本的中间点正好落在竖线上。</p>
<pre><code class="language-python">def changeValue(self, value):
          
    self.c.updateBW.emit(value)        
    self.wid.repaint()</code></pre>
<p>拖动滑块的时候，调用了<code>changeValue()</code>方法。这个方法内部，我们自定义了一个可以传参的updateBW信号。参数就是滑块的当前位置。这个数值之后还用来于Burning组件，然后重新渲染Burning组件。</p>
<figure>
<img class="whitelist" src="docs/PyQt5/images/10-burning.png" alt="burning widget" />
</figure>
